热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

文中|可能会_用户态tcpdump如何实现抓到内核网络包的?

篇首语:本文由编程笔记#小编为大家整理,主要介绍了用户态tcpdump如何实现抓到内核网络包的?相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了用户态 tcpdump 如何实现抓到内核网络包的?相关的知识,希望对你有一定的参考价值。




作者 | 张彦飞allen


来源 | 开发内功修炼


今天聊聊大家工作中经常用到的 tcpdump。


在网络包的发送和接收过程中,绝大部分的工作都是在内核态完成的。那么问题来了,我们常用的运行在用户态的程序 tcpdump 是那如何实现抓到内核态的包的呢?有的同学知道 tcpdump 是基于 libpcap 的,那么 libpcap 的工作原理又是啥样的呢。如果让你裸写一个抓包程序,你有没有思路?


按照飞哥的风格,不搞到最底层的原理咱是不会罢休的。所以我对相关的源码进行了深入分析。通过本文,你将彻底搞清楚了以下这几个问题。


  • tcpdump 是如何工作的?

  • netfilter 过滤的包 tcpdump 是否可以抓的到?

  • 让你自己写一个抓包程序的话该如何下手?


借助这几个问题,我们来展开今天的探索之旅!



一、网络包接收过程


在图解Linux网络包接收过程一文中我们详细介绍了网络包是如何从网卡到达用户进程中的。这个过程我们可以简单用如下这个图来表示。



找到 tcpdump 抓包点


我们在网络设备层的代码里找到了 tcpdump 的抓包入口。在 __netif_receive_skb_core 这个函数里会遍历 ptype_all 上的协议。还记得上文中我们提到 tcpdump 在 ptype_all 上注册了虚拟协议。这时就能执行的到了。来看函数:


//file: net/core/dev.c
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
    ......
    //遍历 ptype_all (tcpdump 在这里挂了虚拟协议)
    list_for_each_entry_rcu(ptype, &ptype_all, list) 
        if (!ptype->dev || ptype->dev == skb->dev) 
            if (pt_prev)
                ret = deliver_skb(skb, pt_prev, orig_dev);
            pt_prev = ptype;
        
    

在上面函数中遍历 ptype_all,并使用 deliver_skb 来调用协议中的回调函数。


//file: net/core/dev.c 
static inline int deliver_skb(...)
 return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);

对于 tcpdump 来说,就会进入 packet_rcv 了(后面我们再说为啥是进入这个函数)。这个函数在 net/packet/af_packet.c 文件中。


//file: net/packet/af_packet.c
static int packet_rcv(struct sk_buff *skb, ...)
 __skb_queue_tail(&sk->sk_receive_queue, skb);
 ......

可见 packet_rcv 把收到的 skb 放到了当前 packet socket 的接收队列里了。这样后面调用 recvfrom 的时候就可以获取到所抓到的包!!


再找 netfilter 过滤点


为了解释我们开篇中提到的问题,这里我们再稍微到协议层中多看一些。在 ip_rcv 中我们找到了一个 netfilter 相关的执行逻辑。


//file: net/ipv4/ip_input.c
int ip_rcv(...)
 ......
 return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
         ip_rcv_finish);

如果你用 NF_HOOK 作为关键词来搜索,还能搜到不少 netfilter 的过滤点。不过所有的过滤点都是位于 IP 协议层的。


在接收包的过程中,数据包是先经过网络设备层然后才到协议层的。



那么我们开篇中的一个问题就有了答案了。假如我们设置了 netfilter 规则,在接收包的过程中,工作在网络设备层的 tcpdump 先开始工作。还没等 netfilter 过滤,tcpdump 就抓到包了!


所以,在接收包的过程中,netfilter 过滤并不会影响 tcpdump 的抓包!



二、网络包发送过程


我们接着再来看网络包发送过程。



找到 netfilter 过滤点


在发送的过程中,同样是在 IP 层进入各种 netfilter 规则的过滤。


//file: net/ipv4/ip_output.c  
int ip_local_out(struct sk_buff *skb)
 //执行 netfilter 过滤
 err = __ip_local_out(skb);
int __ip_local_out(struct sk_buff *skb)
 ......
 return nf_hook(NFPROTO_IPV4, NF_INET_LOCAL_OUT, skb, NULL,
         skb_dst(skb)->dev, dst_output);

在这个文件中,还能看到若干处 netfilter 过滤逻辑。


找到 tcpdump 抓包点


发送过程在协议层处理完毕到达网络设备层的时候,也有 tcpdump 的抓包点。


//file: net/core/dev.c
int dev_hard_start_xmit(struct sk_buff *skb, struct net_device *dev,
   struct netdev_queue *txq)
 ...
 if (!list_empty(&ptype_all))
  dev_queue_xmit_nit(skb, dev);
static void dev_queue_xmit_nit(struct sk_buff *skb, struct net_device *dev)
 list_for_each_entry_rcu(ptype, &ptype_all, list) 
  if ((ptype->dev == dev || !ptype->dev) &&
      (!skb_loop_sk(ptype, skb))) 
   if (pt_prev) 
    deliver_skb(skb2, pt_prev, skb->dev);
    pt_prev = ptype;
    continue;
   
  ......
  
  

在上述代码中我们看到,在 dev_queue_xmit_nit 中遍历 ptype_all 中的协议,并依次调用 deliver_skb。这就会执行到 tcpdump 挂在上面的虚拟协议。


在网络包的发送过程中,和接收过程恰好相反,是协议层先处理、网络设备层后处理。



如果 netfilter 设置了过滤规则,那么在协议层就直接过滤掉了。在下层网络设备层工作的 tcpdump 将无法再捕获到该网络包



三、TCPDUMP 启动


前面两小节我们说到了内核收发包都通过遍历 ptype_all 来执行抓包的。那么我们现在来看看用户态的 tcpdump 是如何挂载协议到内 ptype_all 上的。


我们通过 strace 命令我们抓一下 tcpdump 命令的系统调用,显示结果中有一行 socket 系统调用。Tcpdump 秘密的源头就藏在这行对 socket 函数的调用里。


# strace tcpdump -i eth0
socket(AF_PACKET, SOCK_RAW, 768)
......

socket 系统调用的第一个参数表示创建的 socket 所属的地址簇或者协议簇,取值以 AF 或者 PF 开头。在 Linux 里,支持很多种协议族,在 include/linux/socket.h 中可以找到所有的定义。这里创建的是 packet 类型的 socket。



协议族和地址族:每一种协议族都有其对应的地址族。比如 IPV4 的协议族定义叫 PF_INET,其地址族的定义是 AF_INET。它们是一一对应的,而且值也完全一样,所以经常混用。



//file: include/linux/socket.h
#define AF_UNSPEC 0
#define AF_UNIX  1 /* Unix domain sockets   */
#define AF_LOCAL 1 /* POSIX name for AF_UNIX */
#define AF_INET  2 /* Internet IP Protocol  */
#define AF_INET6 10 /* IP version 6   */
#define AF_PACKET 17 /* Packet family  */
......

另外上面第三个参数 768 代表的是 ETH_P_ALL,socket.htons(ETH_P_ALL) = 768。


我们来展开看这个 packet 类型的 socket 创建的过程中都干了啥,找到 socket 创建源码。


//file: net/socket.c
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol) 
 ......
 retval = sock_create(family, type, protocol, &sock); 
int __sock_create(struct net *net, int family, int type, ...)
 ......
 pf = rcu_dereference(net_families[family]);
 err = pf->create(net, sock, protocol, kern);

在 __sock_create 中,从 net_families 中获取了指定协议。并调用了它的 create 方法来完成创建。


net_families 是一个数组,除了我们常用的 PF_INET( ipv4 ) 外,还支持很多种协议族。比如 PF_UNIX、PF_INET6(ipv6)、PF_PACKET等等。每一种协议族在 net_families 数组的特定位置都可以找到其 family 类型。在这个 family 类型里,成员函数 create 指向该协议族的对应创建函数。



根据上图,我们看到对于 packet 类型的 socket,pf->create 实际调用到的是 packet_create 函数。我们进入到这个函数中来一探究竟,这是理解 tcpdump 工作原理的关键!


//file: packet/af_packet.c
static int packet_create(struct net *net, struct socket *sock, int protocol,
    int kern)
 ...
 po = pkt_sk(sk);
 po->prot_hook.func = packet_rcv;
 //注册钩子
 if (proto) 
  po->prot_hook.type = proto;
  register_prot_hook(sk);
 
static void register_prot_hook(struct sock *sk)
 struct packet_sock *po = pkt_sk(sk);
 dev_add_pack(&po->prot_hook);

在 packet_create 中设置回调函数为 packet_rcv,再通过 register_prot_hook => dev_add_pack 完成注册。注册完后,是在全局协议 ptype_all 链表中添加了一个虚拟的协议进来。



我们再来看下 dev_add_pack 是如何注册协议到 ptype_all 中的。回顾我们开头看到的 socket 函数调用,第三个参数 proto 传入的是 ETH_P_ALL。那 dev_add_pack 其实最后是把 hook 函数添加到了 ptype_all 里了,代码如下。


//file: net/core/dev.c
void dev_add_pack(struct packet_type *pt)
 struct list_head *head = ptype_head(pt);
 list_add_rcu(&pt->list, head);
static inline struct list_head *ptype_head(const struct packet_type *pt)
 if (pt->type == htons(ETH_P_ALL))
  return &ptype_all;
 else
  return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];


我们整篇文章都以 ETH_P_ALL 为例,但其实有的时候也会有其它情况。在别的情况下可能会注册协议到 ptype_base 里了,而不是 ptype_all。同样, ptype_base 中的协议也会在发送和接收的过程中被执行到。



总结:tcpdump 启动的时候内部逻辑其实很简单,就是在 ptype_all 中注册了一个虚拟协议而已。



四、总结


现在我们再回头看开篇提到的几个问题。


1. tcpdump是如何工作的


用户态 tcpdump 命令是通过 socket 系统调用,在内核源码中用到的 ptype_all 中挂载了函数钩子上去。无论是在网络包接收过程中,还是在发送过程中,都会在网络设备层遍历 ptype_all 中的协议,并执行其中的回调。tcpdump 命令就是基于这个底层原理来工作的。


2. netfilter 过滤的包 tcpdump是否可以抓的到
关于这个问题,得分接收和发送过程分别来看。在网络包接收的过程中,由于 tcpdump 近水楼台先得月,所以完全可以捕获到命中 netfilter 过滤规则的包。



但是在发送的过程中,恰恰相反。网络包先经过协议层,这时候被 netfilter 过滤掉的话,底层工作的 tcpdump 还没等看见就啥也没了。



3. 让你自己写一个抓包程序的话该如何下手
如果你想自己写一段类似 tcpdump 的抓包程序的话,使用 packet socket 就可以了。我用 c 写了一段抓包,并且解析源 IP 和目的 IP 的简单 demo。


源码地址:


https://github.com/yanfeizhang/coder-kung-fu/blob/main/tests/network/test04/main.c


编译一下,注意运行需要 root 权限。


# gcc -o main main.c
# ./main

运行结果预览如下:





往期推荐


协程到底有什么用?6种I/O模式告诉你!


在 Kubernetes 上部署 Secret 加密系统 Vault


Redis 内存满了怎么办?这样置才正确!


超值!Docker 常用命令汇总



点分享



点收藏



点点赞



点在看


推荐阅读
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • 本文整理了315道Python基础题目及答案,帮助读者检验学习成果。文章介绍了学习Python的途径、Python与其他编程语言的对比、解释型和编译型编程语言的简述、Python解释器的种类和特点、位和字节的关系、以及至少5个PEP8规范。对于想要检验自己学习成果的读者,这些题目将是一个不错的选择。请注意,答案在视频中,本文不提供答案。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • 本文详细介绍了SQL日志收缩的方法,包括截断日志和删除不需要的旧日志记录。通过备份日志和使用DBCC SHRINKFILE命令可以实现日志的收缩。同时,还介绍了截断日志的原理和注意事项,包括不能截断事务日志的活动部分和MinLSN的确定方法。通过本文的方法,可以有效减小逻辑日志的大小,提高数据库的性能。 ... [详细]
  • 本文介绍了使用PHP实现断点续传乱序合并文件的方法和源码。由于网络原因,文件需要分割成多个部分发送,因此无法按顺序接收。文章中提供了merge2.php的源码,通过使用shuffle函数打乱文件读取顺序,实现了乱序合并文件的功能。同时,还介绍了filesize、glob、unlink、fopen等相关函数的使用。阅读本文可以了解如何使用PHP实现断点续传乱序合并文件的具体步骤。 ... [详细]
  • 如何用JNI技术调用Java接口以及提高Java性能的详解
    本文介绍了如何使用JNI技术调用Java接口,并详细解析了如何通过JNI技术提高Java的性能。同时还讨论了JNI调用Java的private方法、Java开发中使用JNI技术的情况以及使用Java的JNI技术调用C++时的运行效率问题。文章还介绍了JNIEnv类型的使用方法,包括创建Java对象、调用Java对象的方法、获取Java对象的属性等操作。 ... [详细]
  • 本文介绍了PHP常量的定义和使用方法,包括常量的命名规则、大小写敏感性、全局范围和标量数据的限制。同时还提到了应尽量避免定义resource常量,并给出了使用define()函数定义常量的示例。 ... [详细]
  • Python使用Pillow包生成验证码图片的方法
    本文介绍了使用Python中的Pillow包生成验证码图片的方法。通过随机生成数字和符号,并添加干扰象素,生成一幅验证码图片。需要配置好Python环境,并安装Pillow库。代码实现包括导入Pillow包和随机模块,定义随机生成字母、数字和字体颜色的函数。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
  • MySQL数据库锁机制及其应用(数据库锁的概念)
    本文介绍了MySQL数据库锁机制及其应用。数据库锁是计算机协调多个进程或线程并发访问某一资源的机制,在数据库中,数据是一种供许多用户共享的资源,如何保证数据并发访问的一致性和有效性是数据库必须解决的问题。MySQL的锁机制相对简单,不同的存储引擎支持不同的锁机制,主要包括表级锁、行级锁和页面锁。本文详细介绍了MySQL表级锁的锁模式和特点,以及行级锁和页面锁的特点和应用场景。同时还讨论了锁冲突对数据库并发访问性能的影响。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • 大数据Hadoop生态(20)MapReduce框架原理OutputFormat的开发笔记
    本文介绍了大数据Hadoop生态(20)MapReduce框架原理OutputFormat的开发笔记,包括outputFormat接口实现类、自定义outputFormat步骤和案例。案例中将包含nty的日志输出到nty.log文件,其他日志输出到other.log文件。同时提供了一些相关网址供参考。 ... [详细]
  • 【重识云原生】第四章云网络4.8.3.2节——Open vSwitch工作原理详解
    2OpenvSwitch架构2.1OVS整体架构ovs-vswitchd:守护程序,实现交换功能,和Linux内核兼容模块一起,实现基于流的交换flow-basedswitchin ... [详细]
  • 本文介绍了如何通过维持两个堆来获取一个数据流中的中位数。通过使用最大堆和最小堆,分别保存数据流中较小的一半和较大的一半数值,可以保证两个堆的大小差距为1或0。如果数据流中的数量为奇数,则中位数为较大堆的最大值;如果数量为偶数,则中位数为较大堆的最大值和较小堆的最小值的平均值。可以使用优先队列来实现堆的功能。本文还提供了相应的Java代码实现。 ... [详细]
author-avatar
手机用户2502939381
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有